<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
</head>
<body>

                    <h4>Day 15 - 部署Web App</h4>
                    <div class="x-wiki-info"><span>90次阅读</span></div>
                    <hr style="border-top-color:#ccc" />
                    <div class="x-wiki-content x-content"><p>作为一个合格的开发者，在本地环境下完成开发还远远不够，我们需要把Web App部署到远程服务器上，这样，广大用户才能访问到网站。</p>
<p>很多做开发的同学把部署这件事情看成是运维同学的工作，这种看法是完全错误的。首先，最近流行<a href="http://zh.wikipedia.org/wiki/DevOps">DevOps</a>理念，就是说，开发和运维要变成一个整体。其次，运维的难度，其实跟开发质量有很大的关系。代码写得垃圾，运维再好也架不住天天挂掉。最后，DevOps理念需要把运维、监控等功能融入到开发中。你想服务器升级时不中断用户服务？那就得在开发时考虑到这一点。</p>
<p>下面，我们就来把awesome-python-webapp部署到Linux服务器。</p>
<h3 id="-linux-">搭建Linux服务器</h3>
<p>要部署到Linux，首先得有一台Linux服务器。要在公网上体验的同学，可以在Amazon的<a href="http://aws.amazon.com/">AWS</a>申请一台EC2虚拟机（免费使用1年），或者使用国内的一些云服务器，一般都提供Ubuntu Server的镜像。想在本地部署的同学，请安装虚拟机，推荐使用<a href="https://www.virtualbox.org/">VirtualBox</a>。</p>
<p>我们选择的Linux服务器版本是<a href="http://www.ubuntu.com/download/server">Ubuntu Server 12.04 LTS</a>，原因是apt太简单了。如果你准备使用其他Linux版本，也没有问题。</p>
<p>Linux安装完成后，请确保ssh服务正在运行，否则，需要通过apt安装：</p>
<pre><code>$ sudo apt-get openssh-server
</code></pre><p>有了ssh服务，就可以从本地连接到服务器上。建议把公钥复制到服务器端用户的<code>.ssh/authorized_keys</code>中，这样，就可以通过证书实现无密码连接。</p>
<h3 id="-">部署方式</h3>
<p>在本地开发时，我们可以用Python自带的WSGI服务器，但是，在服务器上，显然不能用自带的这个开发版服务器。可以选择的WSGI服务器很多，我们选<a href="http://gunicorn.org/">gunicorn</a>：它用类似Nginx的Master-Worker模式，同时可以提供gevent的支持，不用修改代码，就能获得极高的性能。</p>
<p>此外，我们还需要一个高性能Web服务器，这里选择Nginx，它可以处理静态资源，同时作为反向代理把动态请求交给gunicorn处理。gunicorn负责调用我们的Python代码，这个模型如下：</p>
<p><img src="http://www.liaoxuefeng.com/files/attachments/00140263016247785e90c8e1ea64cc5acfed8da9cae2e86000" alt="nginx-gunicorn-awesome-mysql"></p>
<p>Nginx负责分发请求：</p>
<p><img src="http://www.liaoxuefeng.com/files/attachments/001402630880878c6b29d20770442afbc715f15f2a7afdc000" alt="nginx-as-reverse-proxy"></p>
<p>在服务器端，我们需要定义好部署的目录结构：</p>
<pre><code>/
+- srv/
   +- awesome/       &lt;-- Web App根目录
      +- www/        &lt;-- 存放Python源码
      |  +- static/  &lt;-- 存放静态资源文件
      +- log/        &lt;-- 存放log
</code></pre><p>在服务器上部署，要考虑到新版本如果运行不正常，需要回退到旧版本时怎么办。每次用新的代码覆盖掉旧的文件是不行的，需要一个类似版本控制的机制。由于Linux系统提供了软链接功能，所以，我们把<code>www</code>作为一个软链接，它指向哪个目录，哪个目录就是当前运行的版本：</p>
<p><img src="http://www.liaoxuefeng.com/files/attachments/001402630652247e33b7c5edbb641d89f829d617a1a4aaf000" alt="linux-www-symbol-link"></p>
<p>而Nginx和gunicorn的配置文件只需要指向<code>www</code>目录即可。</p>
<p>Nginx可以作为服务进程直接启动，但gunicorn还不行，所以，<a href="http://supervisord.org/">Supervisor</a>登场！Supervisor是一个管理进程的工具，可以随系统启动而启动服务，它还时刻监控服务进程，如果服务进程意外退出，Supervisor可以自动重启服务。</p>
<p>总结一下我们需要用到的服务有：</p>
<ul>
<li><p>Nginx：高性能Web服务器+负责反向代理；</p>
</li>
<li><p>gunicorn：高性能WSGI服务器；</p>
</li>
<li><p>gevent：把Python同步代码变成异步协程的库；</p>
</li>
<li><p>Supervisor：监控服务进程的工具；</p>
</li>
<li><p>MySQL：数据库服务。</p>
</li>
</ul>
<p>在Linux服务器上用apt可以直接安装上述服务：</p>
<pre><code>$ sudo apt-get install nginx gunicorn python-gevent supervisor mysql-server
</code></pre><p>然后，再把我们自己的Web App用到的Python库安装了：</p>
<pre><code>$ sudo apt-get install python-jinja2 python-mysql.connector
</code></pre><p>在服务器上创建目录<code>/srv/awesome/</code>以及相应的子目录。</p>
<p>在服务器上初始化MySQL数据库，把数据库初始化脚本<code>schema.sql</code>复制到服务器上执行：</p>
<pre><code>$ mysql -u root -p &lt; schema.sql
</code></pre><p>服务器端准备就绪。</p>
<h3 id="-">部署</h3>
<p>用FTP还是SCP还是rsync复制文件？如果你需要手动复制，用一次两次还行，一天如果部署50次不但慢、效率低，而且容易出错。</p>
<p>正确的部署方式是使用工具配合脚本完成自动化部署。<a href="http://www.fabfile.org/">Fabric</a>就是一个自动化部署工具。由于Fabric是用Python开发的，所以，部署脚本也是用Python来编写，非常方便！</p>
<p>要用Fabric部署，需要在本机（是开发机器，不是Linux服务器）安装Fabric：</p>
<pre><code>$ easy_install fabric
</code></pre><p>Linux服务器上不需要安装Fabric，Fabric使用SSH直接登录服务器并执行部署命令。</p>
<p>下一步是编写部署脚本。Fabric的部署脚本叫<code>fabfile.py</code>，我们把它放到<code>awesome-python-webapp</code>的目录下，与<code>www</code>目录平级：</p>
<pre><code>awesome-python-webapp/
+- fabfile.py
+- www/
+- ...
</code></pre><p>Fabric的脚本编写很简单，首先导入Fabric的API，设置部署时的变量：</p>
<pre><code># fabfile.py
import os, re
from datetime import datetime

# 导入Fabric API:
from fabric.api import *

# 服务器登录用户名:
env.user = &#39;michael&#39;
# sudo用户为root:
env.sudo_user = &#39;root&#39;
# 服务器地址，可以有多个，依次部署:
env.hosts = [&#39;192.168.0.3&#39;]

# 服务器MySQL用户名和口令:
db_user = &#39;www-data&#39;
db_password = &#39;www-data&#39;
</code></pre><p>然后，每个Python函数都是一个任务。我们先编写一个打包的任务：</p>
<pre><code>_TAR_FILE = &#39;dist-awesome.tar.gz&#39;

def build():
    includes = [&#39;static&#39;, &#39;templates&#39;, &#39;transwarp&#39;, &#39;favicon.ico&#39;, &#39;*.py&#39;]
    excludes = [&#39;test&#39;, &#39;.*&#39;, &#39;*.pyc&#39;, &#39;*.pyo&#39;]
    local(&#39;rm -f dist/%s&#39; % _TAR_FILE)
    with lcd(os.path.join(os.path.abspath(&#39;.&#39;), &#39;www&#39;)):
        cmd = [&#39;tar&#39;, &#39;--dereference&#39;, &#39;-czvf&#39;, &#39;../dist/%s&#39; % _TAR_FILE]
        cmd.extend([&#39;--exclude=\&#39;%s\&#39;&#39; % ex for ex in excludes])
        cmd.extend(includes)
        local(&#39; &#39;.join(cmd))
</code></pre><p>Fabric提供<code>local(&#39;...&#39;)</code>来运行本地命令，<code>with lcd(path)</code>可以把当前命令的目录设定为<code>lcd()</code>指定的目录，注意Fabric只能运行命令行命令，Windows下可能需要<a href="http://cygwin.com/">Cgywin</a>环境。</p>
<p>在<code>awesome-python-webapp</code>目录下运行：</p>
<pre><code>$ fab build
</code></pre><p>看看是否在<code>dist</code>目录下创建了<code>dist-awesome.tar.gz</code>的文件。</p>
<p>打包后，我们就可以继续编写<code>deploy</code>任务，把打包文件上传至服务器，解压，重置<code>www</code>软链接，重启相关服务：</p>
<pre><code>_REMOTE_TMP_TAR = &#39;/tmp/%s&#39; % _TAR_FILE
_REMOTE_BASE_DIR = &#39;/srv/awesome&#39;

def deploy():
    newdir = &#39;www-%s&#39; % datetime.now().strftime(&#39;%y-%m-%d_%H.%M.%S&#39;)
    # 删除已有的tar文件:
    run(&#39;rm -f %s&#39; % _REMOTE_TMP_TAR)
    # 上传新的tar文件:
    put(&#39;dist/%s&#39; % _TAR_FILE, _REMOTE_TMP_TAR)
    # 创建新目录:
    with cd(_REMOTE_BASE_DIR):
        sudo(&#39;mkdir %s&#39; % newdir)
    # 解压到新目录:
    with cd(&#39;%s/%s&#39; % (_REMOTE_BASE_DIR, newdir)):
        sudo(&#39;tar -xzvf %s&#39; % _REMOTE_TMP_TAR)
    # 重置软链接:
    with cd(_REMOTE_BASE_DIR):
        sudo(&#39;rm -f www&#39;)
        sudo(&#39;ln -s %s www&#39; % newdir)
        sudo(&#39;chown www-data:www-data www&#39;)
        sudo(&#39;chown -R www-data:www-data %s&#39; % newdir)
    # 重启Python服务和nginx服务器:
    with settings(warn_only=True):
        sudo(&#39;supervisorctl stop awesome&#39;)
        sudo(&#39;supervisorctl start awesome&#39;)
        sudo(&#39;/etc/init.d/nginx reload&#39;)
</code></pre><p>注意<code>run()</code>函数执行的命令是在服务器上运行，<code>with cd(path)</code>和<code>with lcd(path)</code>类似，把当前目录在服务器端设置为<code>cd()</code>指定的目录。如果一个命令需要sudo权限，就不能用<code>run()</code>，而是用<code>sudo()</code>来执行。</p>
<h3 id="-supervisor">配置Supervisor</h3>
<p>上面让Supervisor重启gunicorn的命令会失败，因为我们还没有配置Supervisor呢。</p>
<p>编写一个Supervisor的配置文件<code>awesome.conf</code>，存放到<code>/etc/supervisor/conf.d/</code>目录下：</p>
<pre><code>[program:awesome]
command     = /usr/bin/gunicorn --bind 127.0.0.1:9000 --workers 1 --worker-class gevent wsgiapp:application
directory   = /srv/awesome/www
user        = www-data
startsecs   = 3

redirect_stderr         = true
stdout_logfile_maxbytes = 50MB
stdout_logfile_backups  = 10
stdout_logfile          = /srv/awesome/log/app.log
</code></pre><p>配置文件通过<code>[program:awesome]</code>指定服务名为<code>awesome</code>，<code>command</code>指定启动gunicorn的命令行，设定gunicorn的启动端口为9000，WSGI处理函数入口为<code>wsgiapp:application</code>。</p>
<p>然后重启Supervisor后，就可以随时启动和停止Supervisor管理的服务了：</p>
<pre><code>$ sudo supervisorctl reload
$ sudo supervisorctl start awesome
$ sudo supervisorctl status
awesome                RUNNING    pid 1401, uptime 5:01:34
</code></pre><h3 id="-nginx">配置Nginx</h3>
<p>Supervisor只负责运行gunicorn，我们还需要配置Nginx。把配置文件<code>awesome</code>放到<code>/etc/nginx/sites-available/</code>目录下：</p>
<pre><code>server {
    listen      80; # 监听80端口

    root       /srv/awesome/www;
    access_log /srv/awesome/log/access_log;
    error_log  /srv/awesome/log/error_log;

    # server_name awesome.liaoxuefeng.com; # 配置域名

    # 处理静态文件/favicon.ico:
    location /favicon.ico {
        root /srv/awesome/www;
    }

    # 处理静态资源:
    location ~ ^\/static\/.*$ {
        root /srv/awesome/www;
    }

    # 动态请求转发到9000端口(gunicorn):
    location / {
        proxy_pass       http://127.0.0.1:9000;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
}
</code></pre><p>然后在<code>/etc/nginx/sites-enabled/</code>目录下创建软链接：</p>
<pre><code>$ pwd
/etc/nginx/sites-enabled
$ sudo ln -s /etc/nginx/sites-available/awesome .
</code></pre><p>让Nginx重新加载配置文件，不出意外，我们的<code>awesome-python-webapp</code>应该正常运行：</p>
<pre><code>$ sudo /etc/init.d/nginx reload
</code></pre><p>如果有任何错误，都可以在<code>/srv/awesome/log</code>下查找Nginx和App本身的log。如果Supervisor启动时报错，可以在<code>/var/log/supervisor</code>下查看Supervisor的log。</p>
<p>如果一切顺利，你可以在浏览器中访问Linux服务器上的<code>awesome-python-webapp</code>了：</p>
<p><img src="http://www.liaoxuefeng.com/files/attachments/001402634913242c40921a39dfd429f89926687533961e7000" alt="awesome-run-on-server"></p>
<p>如果在开发环境更新了代码，只需要在命令行执行：</p>
<pre><code>$ fab build
$ fab deploy
</code></pre><p>自动部署完成！刷新浏览器就可以看到服务器代码更新后的效果。</p>
<h3 id="-">友情链接</h3>
<p>嫌国外网速慢的童鞋请移步网易和搜狐的镜像站点：</p>
<p><a href="http://mirrors.163.com/">http://mirrors.163.com/</a></p>
<p><a href="http://mirrors.sohu.com/">http://mirrors.sohu.com/</a></p>
</div>

                    <hr style="border-top-color:#ccc" />

                    